Route map, role model, component inventory and data contracts — for sign-off before any screen gets built. Built on your uploaded clean-room roleplay shell, styled only with design-system tokens so the palette stays an open decision.
Fan: invite → apply → wait → approved → first session. Reads end-to-end in the prototype as one continuous click-path.
Creator dashboard · Server home · Character card management · Extended roleplay page. Fan-flow screens ride along because the journey needs them.
One language at a time, switched from the header (EN / 中文) — never both on screen. Every string ships as an EN/ZH pair, so the switch is a data change, not a screen fork.
Immersive: messages float over the character art, exactly as your upload does. Rail, right panel and mobile drawer preserved.
List + detail split on desktop, full-height drawer on mobile. Full trust & safety panel inside the detail column.
Invite code, creator questions, rules agreement, moderator approval, membership expiry, per-character grants — all mandatory. External account link becomes per-server optional.
One states gallery screen holds every loading / empty / error / denied / expired / revoked variant, so product screens stay clean.
Light-blue accent, applied as a token retune only: the ramp is overridden at :root and every surface still reads var(--color-*). Swapping the theme stays a token edit, not a screen edit.
Three-column grid, panel open/closed/auto tri-state, profile ⇄ chat mode switch, 980px drawer breakpoint, 640px bottom-nav breakpoint, reduced-motion guard. All of it stays; the CSS variables at the top of globals.css are re-pointed at design-system tokens.
Composer (view) + useSession() (future API seam)No names, art, descriptions or messages carried over from any third-party product. Every character, server and message in the prototype is invented placeholder content, and art is a drawn placeholder rather than an image.
?next= so an invite link survives sign-inOut of scope for the first prototype pass — routes reserved so the role model stays complete.
Code valid, unexpired, under application cap.
Signed in and verified.
Per-server setting — required, optional, or off. Generic provider slots.
Creator questions answered, rules agreed, source recorded.
Owner or moderator decides. Then membership, group, expiry and per-character grants.
| Nav item | Admin | Owner | Moderator | Member | Applicant | Banned |
|---|---|---|---|---|---|---|
| Home 首页 | ● | ● | ● | ● | — | — |
| My servers 我的服务器 | ● | ● | ● | ● | — | — |
| Characters 角色 | ● | ● | ● | ● granted only | — | — |
| Sessions 会话 | ● | ● | ● | ● | — | — |
| Applications 申请 | ● | ● | ● | ● | ● | ● read-only |
| Account settings 账号设置 | ● | ● | ● | ● | ● | ● |
| Studio overview | ● | ● | — | — | — | — |
| Character cards | ● | ● | ● edit, no publish | — | — | — |
| Applications review | ● | ● | ● | — | — | — |
| Members | ● | ● | ● no ban | — | — | — |
| Invitations | ● | ● | ● | — | — | — |
| Announcements | ● | ● | ● | — | — | — |
| Server settings | ● | ● | — | — | — | — |
type Role = 'admin' | 'owner' | 'moderator' | 'member'
| 'applicant' | 'rejected' | 'banned'
type AppStatus = 'draft' | 'pending' | 'info_required'
| 'approved' | 'rejected' | 'expired' | 'revoked'
interface Server {
id, slug, name, tagline, coverRef
creator: CreatorRef // verified: boolean
rules: string[]
externalAccount: 'required' | 'optional' | 'off'
questions: Question[]
memberGroups: MemberGroup[]
}
interface Invitation {
code, serverId, createdBy
expiresAt, maxApplications, usedCount
singleUse: boolean
requiredProvider?: ProviderId
defaultGroupId?: string
disabled: boolean
}
interface Application {
id, serverId, applicantId, invitationCode
status: AppStatus
displayName, reason, referrer?
answers: Answer[]
externalAccount?: ExternalAccountRef
risk: RiskSignal[] // server-computed
notes: ReviewNote[] // staff-only
submittedAt, decidedAt?, decidedBy?
}
interface CharacterCard {
id, serverId, name, blurb, avatarRef, coverRef
tags: string[]
visibility: 'draft' | 'published' | 'archived'
groupIds: string[]
version: number
ragCorpusLimitTokens: 60000
secondaryKnowledgeEnabled: boolean
ragSearchLimitTokens?: number // 100..14500; omitted = auto
preStatusBarPrompt?: string // legacy import only; editor migrates and clears it
postStatusBarPrompt?: string // legacy import only; editor migrates and clears it
preStatusBarVariables?: StatusVariable[]
postStatusBarVariables?: StatusVariable[]
preStatusBarCss?: string // presentation-only; declarations only
postStatusBarCss?: string // presentation-only; declarations only
globalThemeCss?: string // presentation-only; parsed + scoped
theaterHtml?: string // presentation-only; hostile micro-app
hasSystemPrompt: boolean // never the prompt itself
promptRef: string // opaque server handle
model: ModelSettings // display-safe subset
}
type StatusVariable =
| { id, name, source: 'ai', instruction }
| { id, name, source: 'code',
codeOperation: 'turn_increment' | 'random_character',
increment?: 1..99,
characters?: string /* legacy key: comma-separated string choices */ }
// There is deliberately no arbitrary code/source field.
One lib/api/*.ts module per resource, each exporting async functions with the real signature and a mock implementation behind USE_MOCKS. Screens import the function, never the fixture. Swapping to real endpoints touches only these files.
Composer and MessageList are pure views over useSession(), which today appends to local state and returns a canned reply. A streaming transport drops in behind the same hook.
Model keys and server prompts never enter a client payload. A live creator preview may send its request-owned draft RAG text to the authenticated preview endpoint; persisted roleplay uses opaque server references.
The Secondary information section sits immediately after Writing & roleplay requirements and before Opening & state. It contains automatic token planning, one enable/disable switch, and the three RAG sources: World lore, Character secondary settings, and Trigger Memory. It defaults on for new cards and legacy drafts without an explicit switch value; the author must actively turn it off. Turning it off preserves authored text but sends secondaryKnowledgeEnabled: false, which makes backend retrieval and injection budgets zero. The sources share a 60,000-token corpus limit. One optional per-turn search limit remains inside this section; when omitted the backend computes max(100, ceil(corpusTokens / 100) × 10). Recommend self-contained 50–150-token entries in the form type/title → keywords or aliases → concrete fact, with blank lines between entries and explicit repeated proper nouns. World core exposes only World directive and World description; the editor no longer asks for a separate world name. Writing & roleplay requirements are fixed every-turn structured context with narration, tone, enactment, AI status-variable definitions, output format and avoid rules. Status-variable name, fill type, detail and create action share one desktop row; narrow screens wrap it. There is no shared AI-rule field: legacy shared rules migrate into per-variable instructions and the old field is cleared. Local status variables remain browser-only. The UI no longer exposes Other requirements or When it applies. These fixed model-visible fields consume their actual size before enabled retrieval layers use the remaining 14,500-token author pool. Together with world core and character core they receive explicit Bedrock Claude prompt-cache checkpoints; AI status-variable definitions are part of the writing-and-enactment checkpoint. The complete model input is capped at 45,000 tokens and the current user message has its own 2,000-token ceiling. The authoring UI shows per-field token usage but never renders the assembled fixed prompt/XML. New cards and legacy drafts missing a scope default all three RAG cognition scopes to Public facts, while explicitly stored choices remain unchanged. Opening & state exposes exactly two author fields: authoritative state facts and an opening message capped at 4,000 content tokens; the opening enters history and state uses its separate 2,500-token ceiling. Mini theater & appearance follows Opening & state.
Mini theater appears first and is handled as hostile code in srcdoc with sandbox="allow-scripts" only, an opaque origin, a deny-by-default CSP and a source/origin/channel-checked height-only postMessage protocol. It never receives allow-same-origin. Remote resources, nested frames, object/embed, external scripts, navigation, forms, clipboard, parent DOM, workers and dynamic-code APIs are stripped or browser-blocked. Global beautification follows it and is a restricted theme, not arbitrary page CSS: only the thirteen named #opalia_*/.opalia_* hooks are accepted, every selector is prefixed with #opalia_roleplay_root, and external URLs, global selectors, overlays, z-index, pointer interception, hiding, clipping/masking and unbounded animation are removed. Pre- and post-response status bars use named variables. AI variables send only a name and escaped fill instruction; browser variables are a closed enum containing a system-named round increment with an integer step from 1 through 99 and a system-named deterministic selection of one complete string from comma-separated candidates. On desktop the New row gives each input a content-weighted track and keeps the New action compact; on mobile every control becomes a full-width row. The action and inputs use border-box sizing and the same 44px control height. Added variables appear below the CSS editor as blue capsules that show only their variable names. Clicking one inserts a non-editable blue field with that same name at the current cursor; the editor safely serializes it as a known var(--opalia-status-*) token without parsing authored HTML. Visible text is composed only by the reserved --opalia-status-text declaration, whose parser accepts quoted literals and known variable references without evaluating code. AI definitions are escaped into fixed structured XML and count against the shared author pool; local operations and CSS never enter a model request or token budget. Model-generated JSON values are rendered as text before or after narration. If card persistence moves server-side, this same presentation policy must be revalidated on write and again on render.
Landing → invitation → application → status (pending → approved) → enter server. Clickable straight through.
Server home with announcement, character collection, recent sessions, membership status.
Your shell, extended: server + character switchers, sessions, model placeholder, settings drawer, working composer.
Dashboard, character card editor with eight form sections, review split view, invitations, members.
Gallery of all ten mock states plus the shared empty / skeleton / denied components.